/*
 * Copyright 2007 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dev.jas.as.impl;

import com.google.gwt.dev.js.ast.JsArrayAccess;
import com.google.gwt.dev.js.ast.JsBinaryOperation;
import com.google.gwt.dev.js.ast.JsBinaryOperator;
import com.google.gwt.dev.js.ast.JsBlock;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsExprStmt;
import com.google.gwt.dev.js.ast.JsExpression;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsInvocation;
import com.google.gwt.dev.js.ast.JsModVisitor;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsObjectLiteral;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsPropertyInitializer;
import com.google.gwt.dev.js.ast.JsScope;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.js.ast.JsThisRef;
import com.google.gwt.dev.js.ast.JsReturn;
import com.google.gwt.dev.js.ast.JsVars;

import java.util.List;
import java.util.Stack;

/**
 * Elminates property initializers where the "property" is not a name.
 * For example, we replace this:
 * <blockquote>
 * <code>{ 1:2, 3:4 }</code>
 * </blockquote>
 * with this:
 * <blockquote>
 * <code>(function() { var x = {}; x[1] = 2; x[3] = 4; return x; })()</code>
 * </blockquote>
 *
 * <p>
 * This is necessary to make the program compatible with ActionScript 2.0.
 */
public final class JsObjLitPropCleaner {

  private static class Visitor extends JsModVisitor {

    private final Stack<JsScope> scopes = new Stack<JsScope>();

    protected Visitor(JsScope topScope) {
        scopes.push(topScope);
    }

    @Override
    public boolean visit(JsFunction func, JsContext ctx) {
      scopes.push(func.getScope());
      return true;
    }

    @Override
    public void endVisit(JsFunction func, JsContext ctx) {
      scopes.pop();
    }

    @Override
    public void endVisit(JsObjectLiteral node, JsContext ctx) {

      // Check for disallowed initializer property names
      List<JsPropertyInitializer> propInits = node.getPropertyInitializers();
      boolean replace = false;
      for (JsPropertyInitializer propInit : propInits) {
        if (!(propInit.getLabelExpr() instanceof JsNameRef)) {
          replace = true;
          break;
        }
      }
      if (!replace)
        return;

      // Replace object literal with explicit constructor invocation
      JsFunction func = new JsFunction(node.getSourceInfo(), scopes.peek());
      JsBlock body = new JsBlock(func.getSourceInfo());
      func.setBody(body);
      List<JsStatement> stmts = body.getStatements();
      JsVars vars = new JsVars(func.getSourceInfo());
      JsName name = getTempVarName(scopes.peek());
      JsVars.JsVar var = new JsVars.JsVar(func.getSourceInfo(), name);
      var.setInitExpr(new JsObjectLiteral(func.getSourceInfo()));
      vars.add(var);
      stmts.add(vars);
      for (JsPropertyInitializer propInit : propInits) {
        stmts.add(new JsExprStmt(propInit.getSourceInfo(), new JsBinaryOperation(propInit.getSourceInfo(), JsBinaryOperator.ASG,
          new JsArrayAccess(propInit.getSourceInfo(), new JsNameRef(propInit.getSourceInfo(),name), propInit.getLabelExpr()),
          propInit.getValueExpr())));
      }
      stmts.add(new JsReturn(func.getSourceInfo(), new JsNameRef(func.getSourceInfo(), name)));
      JsInvocation invoke = new JsInvocation(func.getSourceInfo());
      invoke.setQualifier(func);
      ctx.replaceMe(invoke);
    }

    // Get a variable name not conflicting with the current or parent scope
    protected JsName getTempVarName(JsScope scope) {
      for (int i = 0; true; i++) {
        String candidate = "x" + i;
        if (scope.findExistingName(candidate) == null)
          return scope.declareName(candidate);
      }
    }
  }

  public static void exec(JsProgram program) {
    new Visitor(program.getScope()).accept(program);
  }

  private JsObjLitPropCleaner() {
  }
}
